// Node.js ESM server to track visits and record IPs
// - Endpoints:
//   GET/POST /track  -> increment counter, record IP, return summary JSON
//   GET        /stats -> return aggregated stats JSON
//   GET        /pixel -> 1x1 SVG tracking pixel, also records visit
//   GET        /health -> health check
// - Persistence: service/index.txt stores JSON { total, ips, lastAt }

import { createServer } from "http";
import { URL } from "url";
import { promises as fs } from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Data file path for persistence
const DATA_FILE = path.join(__dirname, "index.txt");
// Add atomic write and batching controls
const TMP_FILE = path.join(__dirname, "index.txt.tmp");
const FLUSH_INTERVAL_MS = 500; // batch updates within 500ms to avoid frequent disk writes
let writeInProgress = false;
let pendingWrites = 0;
let scheduledTimer = null;

// In-memory state (loaded from DATA_FILE on startup)
let state = {
  total: 0,
  ips: {}, // { ip: count }
  lastAt: 0,
};

// Read persisted state if available
async function loadState() {
  try {
    const raw = await fs.readFile(DATA_FILE, "utf8");
    if (raw && raw.trim().length > 0) {
      const data = JSON.parse(raw);
      if (data && typeof data === "object") {
        state = {
          total: Number(data.total) || 0,
          ips: data.ips && typeof data.ips === "object" ? data.ips : {},
          lastAt: Number(data.lastAt) || 0,
        };
      }
    }
  } catch (err) {
    // If file doesn't exist, initialize on first write
    if (err.code !== "ENOENT") {
      console.error("[stats] Failed to read state:", err);
    }
  }
}

// Persist current state to disk
async function saveState() {
  // Atomic replace: write to temp then rename to avoid partial writes
  const payload = JSON.stringify(state, null, 2);
  await fs.writeFile(TMP_FILE, payload, "utf8");
  await fs.rename(TMP_FILE, DATA_FILE);
}

// Batch writes: schedule a flush, coalescing high-frequency updates
function scheduleSave() {
  pendingWrites += 1;
  if (!scheduledTimer) {
    scheduledTimer = setTimeout(() => {
      scheduledTimer = null;
      // ensure single writer
      if (writeInProgress) {
        // if a write is ongoing, reschedule shortly
        scheduleSave();
        return;
      }
      writeInProgress = true;
      saveState()
        .catch((err) => {
          console.error("[stats] Failed to save state:", err);
        })
        .finally(() => {
          writeInProgress = false;
          // if more updates arrived during write, schedule another flush
          if (pendingWrites > 0) {
            pendingWrites = 0;
            scheduleSave();
          }
        });
    }, FLUSH_INTERVAL_MS);
  }
}

// Utility: get client IP from request headers or socket
function getClientIp(req) {
  // Prefer headers that may be set by proxies/CDNs
  const xff = req.headers["x-forwarded-for"];
  const cfip = req.headers["cf-connecting-ip"];
  const realIpHeader = req.headers["x-real-ip"];

  let ip = null;
  if (cfip && typeof cfip === "string") ip = cfip;
  else if (realIpHeader && typeof realIpHeader === "string") ip = realIpHeader;
  else if (xff && typeof xff === "string") ip = xff.split(",")[0].trim();

  if (!ip) {
    // Fallback to socket remoteAddress
    ip = req.socket?.remoteAddress || "unknown";
  }

  // Normalize IPv6 localhost ::1 to 127.0.0.1 for readability
  if (ip === "::1") ip = "127.0.0.1";
  return ip;
}

// Update counters for a visit
async function recordVisit(req) {
  const ip = getClientIp(req);
  state.total += 1;
  state.lastAt = Date.now();
  state.ips[ip] = (state.ips[ip] || 0) + 1;
  // Schedule batched persistence to avoid concurrent writes under high load
  scheduleSave();
  return { ip, total: state.total, ipCount: state.ips[ip] };
}

// CORS headers for browser requests
function setCors(res) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}

// Create server
const server = createServer(async (req, res) => {
  setCors(res);

  // Handle preflight
  if (req.method === "OPTIONS") {
    res.statusCode = 204;
    res.end();
    return;
  }

  let urlObj;
  try {
    urlObj = new URL(req.url, `http://${req.headers.host}`);
  } catch (e) {
    res.statusCode = 400;
    res.end("Bad Request");
    return;
  }

  const { pathname } = urlObj;

  // Health check
  if (pathname === "/health") {
    res.statusCode = 200;
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({ ok: true, uptime: process.uptime() }));
    return;
  }

  // Tracking endpoints
  if (
    pathname === "/track" &&
    (req.method === "GET" || req.method === "POST")
  ) {
    const result = await recordVisit(req);
    res.statusCode = 200;
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({ ok: true, ...result }));
    return;
  }

  if (pathname === "/pixel" && req.method === "GET") {
    const result = await recordVisit(req);
    // 1x1 transparent SVG pixel
    const svg =
      '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>';
    res.statusCode = 200;
    res.setHeader("Content-Type", "image/svg+xml");
    res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    // Expose minimal tracking metadata via headers (optional)
    res.setHeader("X-Stats-Total", String(result.total));
    res.setHeader("X-Stats-IP-Count", String(result.ipCount));
    res.end(svg);
    return;
  }

  // Stats report
  if (pathname === "/stats" && req.method === "GET") {
    const uniqueIps = Object.keys(state.ips).length;
    // Build top IP list (up to 20)
    const topIps = Object.entries(state.ips)
      .sort((a, b) => b[1] - a[1])
      .slice(0, 20)
      .map(([ip, count]) => ({ ip, count }));

    res.statusCode = 200;
    res.setHeader("Content-Type", "application/json");
    res.end(
      JSON.stringify({
        ok: true,
        total: state.total,
        uniqueIps,
        lastAt: state.lastAt,
        topIps,
      })
    );
    return;
  }

  // Fallback 404
  res.statusCode = 404;
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({ ok: false, error: "Not Found" }));
});

// Initialize and start server
const PORT = process.env.PORT ? Number(process.env.PORT) : 8787;
loadState()
  .catch(() => {
    /* ignore load errors; will initialize on first write */
  })
  .finally(() => {
    server.listen(PORT, () => {
      console.log(`[stats] Server listening on http://localhost:${PORT}`);
    });
  });
